using SS.Common.Lock;
using SpinWait = System.Threading.SpinWait;

namespace SS.Common.Collection
{
    #region Used namespaces

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;

    #endregion

    /// <summary>
    /// Blocking collection by wrapping a concurrent collection and using spin wait
    /// </summary>
    /// <typeparam name="T">
    /// type of collection item
    /// </typeparam>
    public class BlockingCollection<T> : IEnumerable<T>, ICollection, IDisposable
    {
        /// <summary>
        /// Underlying collection
        /// </summary>
        private readonly IConcurrentCollection<T> underlyingColl;

        /// <summary>
        /// Maximum number of item
        /// </summary>
        private readonly int upperBound;

        /// <summary>
        /// Is collection full
        /// </summary>
        private readonly Func<bool> isFull;

        /// <summary>
        /// SpinWait object for locking
        /// </summary>
        private readonly SpinWait spinWait = new SpinWait();

        /// <summary>
        /// Mark the collection that 
        /// </summary>
        private AtomicBoolean isComplete;

        #region ctors

        /// <summary>
        /// Initializes a new instance of the <see cref="BlockingCollection{T}"/> class. 
        /// Wrapping a ConcurrentQueue underlying
        /// </summary>
        public BlockingCollection()
            : this(new ConcurrentQueueEx<T>(), -1)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="BlockingCollection{T}"/> class. 
        /// </summary>
        /// <param name="upperBound">
        /// Maximum number of item can be stored, -1 mean no limit
        /// </param>
        public BlockingCollection(int upperBound)
            : this(new ConcurrentQueueEx<T>(), upperBound)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="BlockingCollection{T}"/> class. 
        /// </summary>
        /// <param name="underlyingColl">
        /// Underlining collection
        /// </param>
        public BlockingCollection(IConcurrentCollection<T> underlyingColl)
            : this(underlyingColl, -1)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="BlockingCollection{T}"/> class. 
        /// </summary>
        /// <param name="underlyingColl">
        /// Underlying collection
        /// </param>
        /// <param name="upperBound">
        /// Maximum allowed number of items
        /// </param>
        public BlockingCollection(IConcurrentCollection<T> underlyingColl, int upperBound)
        {
            this.underlyingColl = underlyingColl;
            this.upperBound = upperBound;
            if (upperBound == -1)
            {
                this.isFull = delegate { return false; };
            }
            else
            {
                this.isFull = delegate { return underlyingColl.Count >= upperBound; };
            }
        }

        /// <summary>
        /// Finalizes an instance of the <see cref="BlockingCollection{T}"/> class. 
        /// Call to dispose
        /// </summary>
        ~BlockingCollection()
        {
            this.Dispose(false);
        }

        #endregion

        #region Add & Remove (+ Try)

        /// <summary>
        /// Add a new item to the collection
        /// Check for full or completed before add
        /// </summary>
        /// <param name="item">
        /// The item to be added
        /// </param>
        /// <exception cref="InvalidOperationException">
        /// If is full or completed
        /// </exception>
        public void Add(T item)
        {
            while (true)
            {
                while (this.isFull())
                {
                    if (this.isComplete.Value)
                    {
                        throw new InvalidOperationException(
                            "The BlockingCollection<T>" +
                            " has been marked as complete with regards to additions.");
                    }

                    this.Block();
                }

                // Extra check. The status might have changed after Block() or if isFull() is always false
                if (this.isComplete.Value)
                {
                    throw new InvalidOperationException(
                        "The BlockingCollection<T> has" +
                        " been marked as complete with regards to additions.");
                }

                // Go back in main waiting loop
                if (this.isFull())
                {
                    continue;
                }

                if (this.underlyingColl.Add(item))
                {
                    break;
                }
            }
        }

        /// <summary>
        /// Remove the last item
        /// </summary>
        /// <returns>
        /// The last item
        /// </returns>
        public T Remove()
        {
            while (this.underlyingColl.Count == 0)
            {
                if (this.isComplete.Value)
                {
                    throw new OperationCanceledException(
                        "The BlockingCollection<T> is empty and has been marked as complete with regards to additions.");
                }

                this.Block();
            }

            T item;
            this.underlyingColl.Remove(out item);

            return item;
        }

        /// <summary>
        /// Try to add an item
        /// </summary>
        /// <param name="item">
        /// The item to be added
        /// </param>
        /// <returns>
        /// true if add successfully
        /// </returns>
        public bool TryAdd(T item)
        {
            if (this.isComplete.Value || this.isFull())
            {
                return false;
            }

            return this.underlyingColl.Add(item);
        }

        /// <summary>
        /// Try to add an item within a specified ammount of time
        /// </summary>
        /// <param name="item">
        /// The item to be added
        /// </param>
        /// <param name="ts">
        /// The specified ammount of time
        /// </param>
        /// <returns>
        /// true if add successfully
        /// </returns>
        public bool TryAdd(T item, TimeSpan ts)
        {
            return this.TryAdd(item, (int)ts.TotalMilliseconds);
        }

        /// <summary>
        /// Try to add an item within a specified ammount of time
        /// </summary>
        /// <param name="item">
        /// The item to be added
        /// </param>
        /// <param name="millisecondsTimeout">
        /// Ammount of time in milliseconds
        /// </param>
        /// <returns>
        /// true if add successfully
        /// </returns>
        public bool TryAdd(T item, int millisecondsTimeout)
        {
            var stopwatch = Stopwatch.StartNew();
            while (this.isFull())
            {
                if (this.isComplete.Value || stopwatch.ElapsedMilliseconds > millisecondsTimeout)
                {
                    stopwatch.Stop();
                    return false;
                }

                this.Block();
            }

            return this.underlyingColl.Add(item);
        }

        /// <summary>
        /// Try to remove an item by blocking wait
        /// </summary>
        /// <param name="item">
        /// The item has been removed
        /// </param>
        /// <returns>
        /// true if remove successfully
        /// </returns>
        public bool TryRemove(out T item)
        {
            return this.underlyingColl.Remove(out item);
        }

        /// <summary>
        /// Try to remove an item within a specified ammount of time
        /// </summary>
        /// <param name="item">
        /// The item has been removed
        /// </param>
        /// <param name="ts">
        /// The specified ammount of time
        /// </param>
        /// <returns>
        /// true if remove successfully
        /// </returns>
        public bool TryRemove(out T item, TimeSpan ts)
        {
            return this.TryRemove(out item, (int)ts.TotalMilliseconds);
        }

        /// <summary>
        /// Try to add an item within a specified ammount of time
        /// </summary>
        /// <param name="item">
        /// The item to be added
        /// </param>
        /// <param name="millisecondsTimeout">
        /// Ammount of time in milliseconds
        /// </param>
        /// <returns>
        /// true if add successfully
        /// </returns>
        public bool TryRemove(out T item, int millisecondsTimeout)
        {
            var stopwatch = Stopwatch.StartNew();
            while (this.underlyingColl.Count == 0)
            {
                if (this.isComplete.Value || stopwatch.ElapsedMilliseconds > millisecondsTimeout)
                {
                    item = default(T);
                    return false;
                }

                this.Block();
            }

            return this.underlyingColl.Remove(out item);
        }

        #endregion

        #region static methods

        /// <summary>
        /// Check array for null or contains invalid data (null items)
        /// </summary>
        /// <param name="collections">
        /// Collections array for checking
        /// </param>
        private static void CheckArray(ICollection<BlockingCollection<T>> collections)
        {
            if (collections == null)
            {
                throw new ArgumentNullException("collections");
            }

            if (collections.Count == 0 || collections.Where(e => e == null).Any())
            {
                throw new ArgumentException(
                    "The collections argument is a 0-length array or contains a null element.",
                    "collections");
            }
        }

        /// <summary>
        /// Add an item to any collection in the array
        /// </summary>
        /// <param name="collections">
        /// Array of collections to add the item to
        /// </param>
        /// <param name="item">
        /// The item to be added
        /// </param>
        /// <returns>
        /// Index of the chosen array
        /// </returns>
        public static int AddAny(BlockingCollection<T>[] collections, T item)
        {
            CheckArray(collections);
            var index = 0;
            foreach (var coll in collections)
            {
                try
                {
                    coll.Add(item);
                    return index;
                }
                catch (InvalidOperationException)
                {
                    // Ignore exception as expected
                }

                index++;
            }

            return -1;
        }

        /// <summary>
        /// Try to add an item to any collection in the array 
        /// </summary>
        /// <param name="collections">
        /// Array of candidate collections to add item to
        /// </param>
        /// <param name="item">
        /// The item to be added
        /// </param>
        /// <returns>
        /// Index of the chosen collection
        /// </returns>
        public static int TryAddAny(BlockingCollection<T>[] collections, T item)
        {
            CheckArray(collections);
            var index = 0;
            foreach (var coll in collections)
            {
                if (coll.TryAdd(item))
                {
                    return index;
                }

                index++;
            }

            return -1;
        }

        /// <summary>
        /// Try to add an item to any collection in the array in a specificed ammount of time
        /// </summary>
        /// <param name="collections">
        /// Array of candidate collections to add item to
        /// </param>
        /// <param name="item">
        /// The item to be added
        /// </param>
        /// <param name="ts">
        /// The specified ammount of time
        /// </param>
        /// <returns>
        /// Index of the chosen collection
        /// </returns>
        public static int TryAddAny(BlockingCollection<T>[] collections, T item, TimeSpan ts)
        {
            CheckArray(collections);
            var index = 0;
            foreach (var coll in collections)
            {
                if (coll.TryAdd(item, ts))
                {
                    return index;
                }

                index++;
            }

            return -1;
        }

        /// <summary>
        /// Try to add an item to any collection in the array in a specificed ammount of time
        /// </summary>
        /// <param name="collections">
        /// Array of candidate collections to add item to
        /// </param>
        /// <param name="item">
        /// The item to be added
        /// </param>
        /// <param name="millisecondsTimeout">
        /// The specified ammount of time in ms
        /// </param>
        /// <returns>
        /// Index of the chosen collection
        /// </returns>
        public static int TryAddAny(
            BlockingCollection<T>[] collections, T item, int millisecondsTimeout)
        {
            CheckArray(collections);
            var index = 0;
            foreach (var coll in collections)
            {
                if (coll.TryAdd(item, millisecondsTimeout))
                {
                    return index;
                }

                index++;
            }

            return -1;
        }

        /// <summary>
        /// Remove an item to any collection in the array
        /// </summary>
        /// <param name="collections">
        /// Array of candidate collections to remove item
        /// </param>
        /// <param name="item">
        /// The item has been removed
        /// </param>
        /// <returns>
        /// Index of the chosen collection
        /// </returns>
        public static int RemoveAny(BlockingCollection<T>[] collections, out T item)
        {
            item = default(T);
            CheckArray(collections);
            var index = 0;
            foreach (var coll in collections)
            {
                try
                {
                    item = coll.Remove();
                    return index;
                }
                catch (InvalidOperationException)
                {
                    // Expected exception
                }

                index++;
            }

            return -1;
        }

        /// <summary>
        /// Try to remove an item to any collection in the array
        /// </summary>
        /// <param name="collections">
        /// Array of candidate collections to remove item
        /// </param>
        /// <param name="item">
        /// The item has been removed
        /// </param>
        /// <returns>
        /// Index of the chosen collection
        /// </returns>
        public static int TryRemoveAny(BlockingCollection<T>[] collections, out T item)
        {
            item = default(T);

            CheckArray(collections);
            var index = 0;
            foreach (var coll in collections)
            {
                if (coll.TryRemove(out item))
                {
                    return index;
                }

                index++;
            }

            return -1;
        }

        /// <summary>
        /// Try to remove an item to any collection in the array in a specified ammount of time
        /// </summary>
        /// <param name="collections">
        /// Array of candidate collections to remove item
        /// </param>
        /// <param name="item">
        /// The item has been removed
        /// </param>
        /// <param name="ts">
        /// The specified ammount of time
        /// </param>
        /// <returns>
        /// Index of the chosen collection
        /// </returns>
        public static int TryRemoveAny(BlockingCollection<T>[] collections, out T item, TimeSpan ts)
        {
            item = default(T);

            CheckArray(collections);
            var index = 0;
            foreach (var coll in collections)
            {
                if (coll.TryRemove(out item, ts))
                {
                    return index;
                }

                index++;
            }

            return -1;
        }

        /// <summary>
        /// Try to remove an item to any collection in the array in a specified ammount of time
        /// </summary>
        /// <param name="collections">
        /// Array of candidate collections to remove item
        /// </param>
        /// <param name="item">
        /// The item has been removed
        /// </param>
        /// <param name="millisecondsTimeout">
        /// The specified ammount of time in ms
        /// </param>
        /// <returns>
        /// Index of the chosen collection
        /// </returns>
        public static int TryRemoveAny(
            BlockingCollection<T>[] collections, out T item, int millisecondsTimeout)
        {
            item = default(T);

            CheckArray(collections);
            var index = 0;
            foreach (var coll in collections)
            {
                if (coll.TryRemove(out item, millisecondsTimeout))
                {
                    return index;
                }

                index++;
            }

            return -1;
        }

        #endregion

        /// <summary>
        /// Mark the collection to prevent adding new item
        /// </summary>
        public void CompleteAdding()
        {
            this.isComplete = true;
        }

        /// <summary>
        /// Copy items to an array
        /// </summary>
        /// <param name="array">
        /// The destination array
        /// </param>
        /// <param name="index">
        /// Copy only from this index
        /// </param>
        void ICollection.CopyTo(Array array, int index)
        {
            this.underlyingColl.CopyTo(array, index);
        }

        /// <summary>
        /// Copy items to an array
        /// </summary>
        /// <param name="array">
        /// The destination array
        /// </param>
        /// <param name="index">
        /// Copy only from this index
        /// </param>
        public void CopyTo(T[] array, int index)
        {
            this.underlyingColl.CopyTo(array, index);
        }

        /// <summary>
        /// Get the consuming enumerable
        /// </summary>
        /// <returns>
        /// The enumerable
        /// </returns>
        public IEnumerable<T> GetConsumingEnumerable()
        {
            T item;
            while (this.TryRemove(out item))
            {
                yield return item;
            }
        }

        /// <summary>
        /// Get items enumerator
        /// </summary>
        /// <returns>
        /// The enumerator
        /// </returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        /// <summary>
        /// Get items enumerator
        /// </summary>
        /// <returns>
        /// The enumerator
        /// </returns>
        IEnumerator<T> IEnumerable<T>.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        /// <summary>
        /// Get items enumerator
        /// </summary>
        /// <returns>
        /// The enumerator
        /// </returns>
        public IEnumerator<T> GetEnumerator()
        {
            return this.GetConsumingEnumerable().GetEnumerator();
        }

        /// <summary>
        /// Dispose the object
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
        }

        /// <summary>
        /// Dispose any managed resource if there is
        /// Mean for overiding by subclass
        /// </summary>
        /// <param name="managedRes">
        /// Clean up the managed resource
        /// </param>
        protected virtual void Dispose(bool managedRes)
        {
        }

        /// <summary>
        /// Convert to array
        /// </summary>
        /// <returns>
        /// The returned array
        /// </returns>
        public T[] ToArray()
        {
            return this.underlyingColl.ToArray();
        }

        /// <summary>
        /// Method used to stall the thread for a limited period of time before retrying an operation 
        /// </summary>
        private void Block()
        {
            this.spinWait.SpinOnce();
        }

        /// <summary>
        /// Gets predefined upper bound for the collection
        /// </summary>
        public int BoundedCapacity
        {
            get
            {
                return this.upperBound;
            }
        }

        /// <summary>
        /// Gets current items count
        /// </summary>
        public int Count
        {
            get
            {
                return this.underlyingColl.Count;
            }
        }

        /// <summary>
        /// Is completed mark set
        /// </summary>
        public bool IsAddingCompleted
        {
            get
            {
                return this.isComplete.Value;
            }
        }

        /// <summary>
        /// Is completed mark set and no item stored in the collection
        /// </summary>
        public bool IsCompleted
        {
            get
            {
                return this.isComplete.Value && this.underlyingColl.Count == 0;
            }
        }

        /// <summary>
        /// Gets SyncRoot object
        /// </summary>
        /// <value>
        /// The sych object
        /// </value>
        object ICollection.SyncRoot
        {
            get
            {
                return this.underlyingColl.SyncRoot;
            }
        }

        /// <summary>
        /// Indicate this collection is thread safe
        /// Delegate to the underlying collection
        /// </summary>
        bool ICollection.IsSynchronized
        {
            get
            {
                return this.underlyingColl.IsSynchronized;
            }
        }
    }
}
